#include <CoreFoundation/CoreFoundation.h>
#include <JavaScriptCore/JSStringRefCF.h>
#include <JavaScriptCore/JavaScript.h>

#include <iostream>
#include <thread>

using namespace std;
JSGlobalContextRef gctx;
JSObjectRef globalThis;
JSContextGroupRef group;

void initJSC() {
  group = JSContextGroupCreate();
  gctx = JSGlobalContextCreateInGroup(group, NULL);
  globalThis = JSContextGetGlobalObject(gctx);
}

string jsValueToStdString(JSGlobalContextRef &context, JSValueRef &jsValue,
                          const CFIndex size) {
  JSStringRef jsStr = JSValueToStringCopy(context, jsValue, NULL);
  CFStringRef cfStr = JSStringCopyCFString(kCFAllocatorSystemDefault, jsStr);
  char charArr[size];
  bzero(charArr, size);
  CFStringGetCString(cfStr, charArr, size, kCFStringEncodingUTF8);
  string name(charArr);
  JSStringRelease(jsStr);
  return name;
}

string JSStringToStdString(JSStringRef jsstr) {
  size_t maxBuffSize = JSStringGetMaximumUTF8CStringSize(jsstr);
  char *utf8Buf = new char[maxBuffSize];
  size_t bytesWritten = JSStringGetUTF8CString(jsstr, utf8Buf, maxBuffSize);
  string utfStr =
      string(utf8Buf,
             bytesWritten - 1); // last bytes is null \0, std::string dont need
  delete[] utf8Buf;
  return utfStr;
}

JSValueRef myFunction(JSContextRef context, JSObjectRef function,
                      JSObjectRef thisObject, size_t argumentCount,
                      const JSValueRef arguments[], JSValueRef *exception) {
  cout << "call with globalThis.staticFunction" << endl << endl;
  JSStringRef ret = JSStringCreateWithUTF8CString("myFunction returned1111");
  return JSValueMakeString(context, ret);
}

bool hasProperty(JSContextRef ctx, JSObjectRef obj, JSStringRef propertyName) {
  string prop = JSStringToStdString(propertyName);
  cout << "[hasProperty] called: " << prop << endl << endl;
  return true;
}

JSValueRef getProperty(JSContextRef ctx, JSObjectRef obj,
                       JSStringRef propertyName, JSValueRef *ex) {
  string prop = JSStringToStdString(propertyName);
  if (prop == "myFunction") {
    JSValueRef func =
        JSObjectMakeFunctionWithCallback(ctx, propertyName, myFunction);
    return func;
  }
  return JSValueMakeUndefined(ctx);
}

void setGlobalFunction() {
  JSStaticFunction sf = {"myFunction", myFunction,
                         kJSPropertyAttributeReadOnly};
  JSStringRef funcName = JSStringCreateWithUTF8CString(sf.name);
  JSObjectRef funcObj =
      JSObjectMakeFunctionWithCallback(gctx, funcName, sf.callAsFunction);
  JSObjectSetProperty(gctx, globalThis, funcName, funcObj,
                      kJSPropertyAttributeNone, NULL);
  JSStringRelease(funcName);

  JSValueRef exception = nullptr;
  JSStringRef script;
  JSValueRef retRef;
  JSStringRef retStringRef;
  try {
    script = JSStringCreateWithUTF8CString("myFunction();");
    if (exception) {
      cout << "wrong" << endl;
    } else {
      cout << "fine!" << endl;
    }
    retRef = JSEvaluateScript(gctx, script, NULL, NULL, 1, &exception);
    if (exception != nullptr) {
      cout << "1" << endl;
      JSStringRef exStr = JSValueToStringCopy(gctx, exception, nullptr);
      size_t len = JSStringGetLength(exStr);
      char exChars[len];
      JSStringGetUTF8CString(exStr, exChars, len);
      if (exStr)
        JSStringRelease(exStr);
      printf("JavaScript Exception: %s\n", exChars);
      // string exStr = jsValueToStdString(gctx, exception, 60);
      // cout << "js eval error: " << exStr << endl << endl;
    } else {
      retStringRef = JSValueToStringCopy(gctx, retRef, NULL);
      size_t retLen = JSStringGetLength(retStringRef);
      char buf[retLen];
      retLen = JSStringGetUTF8CString(retStringRef, buf, retLen);

      cout << "js Invoked: myFunction() = " << string(buf) << endl << endl;
    }
  } catch (const std::exception &e) {
    std::cerr << "invoke failed" << e.what() << '\n';
  }

  if (script)
    JSStringRelease(script);
  if (retStringRef)
    JSStringRelease(retStringRef);
  if (gctx) {
    cout << "ready to release gctx" << endl << endl;
    JSGlobalContextRelease(gctx);
  }
}

JSClassRef parentClass() {
  JSClassRef clazz;
  JSClassDefinition definition;
  JSStaticFunction sf[] = {
      {"myFunction", myFunction,
       kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete},
      {0, 0, 0}};
  definition.version = 0;
  definition.className = "BuildContext";
  definition.parentClass = nullptr;
  definition.staticValues = nullptr;
  definition.attributes = kJSClassAttributeNone;
  definition.staticFunctions = sf;
  definition.initialize = nullptr;
  definition.finalize = nullptr;
  definition.hasProperty = hasProperty;
  definition.getProperty = getProperty;
  definition.setProperty = nullptr;
  definition.deleteProperty = nullptr;
  definition.getPropertyNames = nullptr;
  definition.callAsFunction = nullptr;
  definition.callAsConstructor = nullptr;
  definition.hasInstance = nullptr;
  definition.convertToType = nullptr;
  clazz = JSClassCreate(&definition);
  return clazz;
}

void setGlobalFunctionWhenInitGlobalContext() {
  group = JSContextGroupCreate();
  JSClassRef clzz = parentClass();
  gctx = JSGlobalContextCreateInGroup(group, clzz);
  globalThis = JSContextGetGlobalObject(gctx);
  JSValueRef exception = nullptr;
  JSStringRef script;
  JSValueRef retRef;
  JSStringRef retStringRef;

  JSStringRef propName = JSStringCreateWithUTF8CString("myFunction");

  JSObjectRef nfunc = JSObjectMakeFunctionWithCallback(gctx, propName, myFunction);

  JSObjectSetProperty(gctx, globalThis, propName, nfunc, kJSClassAttributeNone, nullptr);

  bool hasMyFunctionProp = JSObjectHasProperty(gctx, globalThis, propName);
  cout << " hasFunction: " << hasMyFunctionProp << endl;

  script = JSStringCreateWithUTF8CString("myFunction();");
  retRef = JSEvaluateScript(gctx, script, nullptr, nullptr, 0, &exception);
  if (exception != nullptr) {
    JSStringRef exStr = JSValueToStringCopy(gctx, exception, nullptr);
    size_t len = JSStringGetLength(exStr);
    char exChars[len];
    JSStringGetUTF8CString(exStr, exChars, len);
    if (exStr)
      JSStringRelease(exStr);
    printf("JavaScript Exception: %s\n", exChars);
  } else {
    retStringRef = JSValueToStringCopy(gctx, retRef, NULL);
    size_t retLen = JSStringGetLength(retStringRef);
    char buf[retLen];
    retLen = JSStringGetUTF8CString(retStringRef, buf, retLen);
    cout << "js Invoked: myFunction() = " << string(buf) << endl << endl;
  }
}

int main() {
  // initJSC();
  setGlobalFunctionWhenInitGlobalContext();

  return 0;
}